FormConfigurable improvements

Made the BasecampAgent form configurable
Added specs
Added guard-livereload and guard-rspec
Custom values can be entered for `completable` attributes

Dominik Sander 9 years ago
parent
commit
c1f336d04e

+ 3 - 0
Gemfile

@@ -85,6 +85,9 @@ group :development do
85 85
   gem 'better_errors', '~> 1.1'
86 86
   gem 'binding_of_caller'
87 87
   gem 'quiet_assets'
88
+  gem 'guard'
89
+  gem 'guard-livereload'
90
+  gem 'guard-rspec'
88 91
 end
89 92
 
90 93
 group :development, :test do

+ 31 - 0
Gemfile.lock

@@ -55,6 +55,8 @@ GEM
55 55
       rails (>= 3.1)
56 56
     buftok (0.2.0)
57 57
     builder (3.2.2)
58
+    celluloid (0.15.2)
59
+      timers (~> 1.1.0)
58 60
     chronic (0.10.2)
59 61
     coderay (1.1.0)
60 62
     coffee-rails (4.0.1)
@@ -108,6 +110,9 @@ GEM
108 110
       http_parser.rb (>= 0.6.0)
109 111
     em-socksify (0.3.0)
110 112
       eventmachine (>= 1.0.0.beta.4)
113
+    em-websocket (0.5.1)
114
+      eventmachine (>= 0.12.9)
115
+      http_parser.rb (~> 0.6.0)
111 116
     equalizer (0.0.9)
112 117
     erector (0.10.0)
113 118
       treetop (>= 1.2.3)
@@ -134,6 +139,7 @@ GEM
134 139
     foreman (0.63.0)
135 140
       dotenv (>= 0.7)
136 141
       thor (>= 0.13.6)
142
+    formatador (0.2.5)
137 143
     geokit (1.8.5)
138 144
       multi_json (>= 1.3.2)
139 145
     geokit-rails (2.0.1)
@@ -150,6 +156,19 @@ GEM
150 156
       retriable (>= 1.4)
151 157
       signet (>= 0.5.0)
152 158
       uuidtools (>= 2.1.0)
159
+    guard (2.6.1)
160
+      formatador (>= 0.2.4)
161
+      listen (~> 2.7)
162
+      lumberjack (~> 1.0)
163
+      pry (>= 0.9.12)
164
+      thor (>= 0.18.1)
165
+    guard-livereload (2.2.0)
166
+      em-websocket (~> 0.5)
167
+      guard (~> 2.0)
168
+      multi_json (~> 1.8)
169
+    guard-rspec (4.3.1)
170
+      guard (~> 2.1)
171
+      rspec (>= 2.14, < 4.0)
153 172
     hashie (2.0.5)
154 173
     hike (1.2.3)
155 174
     hipchat (1.2.0)
@@ -178,6 +197,11 @@ GEM
178 197
       addressable (~> 2.3)
179 198
     libv8 (3.16.14.7)
180 199
     liquid (2.6.1)
200
+    listen (2.7.9)
201
+      celluloid (>= 0.15.2)
202
+      rb-fsevent (>= 0.9.3)
203
+      rb-inotify (>= 0.9)
204
+    lumberjack (1.0.9)
181 205
     macaddr (1.7.1)
182 206
       systemu (~> 2.6.2)
183 207
     mail (2.5.4)
@@ -262,6 +286,9 @@ GEM
262 286
       thor (>= 0.18.1, < 2.0)
263 287
     raindrops (0.13.0)
264 288
     rake (10.3.2)
289
+    rb-fsevent (0.9.4)
290
+    rb-inotify (0.9.5)
291
+      ffi (>= 0.5.0)
265 292
     rdoc (4.1.1)
266 293
       json (~> 1.4)
267 294
     ref (1.0.5)
@@ -350,6 +377,7 @@ GEM
350 377
     thor (0.19.1)
351 378
     thread_safe (0.3.4)
352 379
     tilt (1.4.1)
380
+    timers (1.1.0)
353 381
     tins (1.3.2)
354 382
     treetop (1.4.15)
355 383
       polyglot
@@ -438,6 +466,9 @@ DEPENDENCIES
438 466
   geokit (~> 1.8.4)
439 467
   geokit-rails (~> 2.0.1)
440 468
   google-api-client
469
+  guard
470
+  guard-livereload
471
+  guard-rspec
441 472
   hipchat (~> 1.2.0)
442 473
   httparty (~> 0.13)
443 474
   jquery-rails (~> 3.1.0)

+ 25 - 0
Guardfile

@@ -0,0 +1,25 @@
1
+
2
+guard 'livereload' do
3
+  watch(%r{app/views/.+\.(erb|haml|slim)$})
4
+  watch(%r{app/helpers/.+\.rb})
5
+  watch(%r{public/.+\.(css|js|html)})
6
+  watch(%r{config/locales/.+\.yml})
7
+  # Rails Assets Pipeline
8
+  watch(%r{(app|vendor)(/assets/\w+/(.+\.(css|js|html|png|jpg))).*}) { |m| "/assets/#{m[3]}" }
9
+end
10
+
11
+guard :rspec, cmd: 'bundle exec spring rspec' do
12
+  watch(%r{^spec/.+_spec\.rb$})
13
+  watch(%r{^lib/(.+)\.rb$})     { |m| "spec/lib/#{m[1]}_spec.rb" }
14
+  watch('spec/spec_helper.rb')  { "spec" }
15
+
16
+  # Rails example
17
+  watch(%r{^app/(.+)\.rb$})                           { |m| "spec/#{m[1]}_spec.rb" }
18
+  watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$})          { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
19
+  watch(%r{^app/controllers/(.+)_(controller)\.rb$})  { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
20
+  watch(%r{^spec/support/(.+)\.rb$})                  { "spec" }
21
+  watch('config/routes.rb')                           { "spec/routing" }
22
+  watch('app/controllers/application_controller.rb')  { "spec/controllers" }
23
+  watch('spec/rails_helper.rb')                       { "spec" }
24
+end
25
+

+ 6 - 1
app/assets/javascripts/components/form_configurable.js.coffee

@@ -51,9 +51,14 @@ $ ->
51 51
     $("input[role=validatable], select[role=validatable]").trigger('change')
52 52
 
53 53
     $.each $("input[role~=completable]"), (i, input) ->
54
-      $(input).select2
54
+      $(input).select2(
55 55
         data: ->
56 56
           completableDefaultOptions(input)
57
+      ).on("change", (e) ->
58
+        if e.added && e.added.id == 'manualInput'
59
+          $(e.currentTarget).select2("destroy")
60
+          $(e.currentTarget).val(e.removed.id)
61
+      )
57 62
 
58 63
     $("input[role~=completable]").on 'select2-open', (e) ->
59 64
       form_data = getFormData(e.currentTarget)

+ 0 - 9
app/concerns/form_configurable.rb

@@ -43,15 +43,6 @@ module FormConfigurable
43 43
         options[:roles] = [options[:roles]]
44 44
       end
45 45
 
46
-      if options[:roles].include?(:completable) && !self.method_defined?("complete_#{name}".to_sym)
47
-        # Not really sure, but method_defined? does not seem to work because we do not have the 'full' Agent class here
48
-        #raise ArgumentError.new("'complete_#{name}' needs to be defined to validate '#{name}'")
49
-      end
50
-
51
-      if options[:roles].include?(:validatable) && !self.method_defined?("validate_#{name}".to_sym)
52
-        #raise ArgumentError.new("'validate_#{name}' needs to be defined to validate '#{name}'")
53
-      end
54
-
55 46
       _form_configurable_fields[name] = options
56 47
     end
57 48
 

+ 22 - 13
app/models/agents/basecamp_agent.rb

@@ -1,23 +1,16 @@
1 1
 module Agents
2 2
   class BasecampAgent < Agent
3 3
     include FormConfigurable
4
-
5
-    cannot_receive_events!
6
-
7 4
     include Oauthable
8 5
     valid_oauth_providers :'37signals'
9 6
 
7
+    cannot_receive_events!
8
+
10 9
     description <<-MD
11 10
       The BasecampAgent checks a Basecamp project for new Events
12 11
 
13 12
       To be able to use this Agent you need to authenticate with 37signals in the [Services](/services) section first.
14 13
 
15
-      You need to provide the `project_id` of the project you want to monitor.
16
-      If you have your Basecamp project opened in your browser you can find the user_id and project_id as follows:
17
-
18
-      `https://basecamp.com/123456/projects/`
19
-      project_id
20
-      `-explore-basecamp`
21 14
     MD
22 15
 
23 16
     event_description <<-MD
@@ -52,6 +45,14 @@ module Agents
52 45
       }
53 46
     end
54 47
 
48
+    form_configurable :project_id, roles: :completable
49
+
50
+    def complete_project_id
51
+      service.prepare_request
52
+      response = HTTParty.get projects_url, request_options.merge(query_parameters)
53
+      response.map { |p| {name: "#{p['name']} (#{p['id']})", value: p['id']}}
54
+    end
55
+
55 56
     def validate_options
56 57
       errors.add(:base, "you need to specify the basecamp project id of which you want to receive events") unless options['project_id'].present?
57 58
     end
@@ -62,8 +63,8 @@ module Agents
62 63
 
63 64
     def check
64 65
       service.prepare_request
65
-      reponse = HTTParty.get request_url, request_options.merge(query_parameters)
66
-      events = JSON.parse(reponse.body)
66
+      response = HTTParty.get events_url, request_options.merge(query_parameters)
67
+      events = JSON.parse(response.body)
67 68
       if !memory[:last_event].nil?
68 69
         events.each do |event|
69 70
           create_event :payload => event
@@ -74,8 +75,16 @@ module Agents
74 75
     end
75 76
 
76 77
   private
77
-    def request_url
78
-      "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json"
78
+    def base_url
79
+      "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/"
80
+    end
81
+
82
+    def events_url
83
+      base_url + "projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json"
84
+    end
85
+
86
+    def projects_url
87
+      base_url + "projects.json"
79 88
     end
80 89
 
81 90
     def request_options

+ 11 - 4
app/models/agents/hipchat_agent.rb

@@ -17,8 +17,10 @@ module Agents
17 17
 
18 18
       Change the `room_name` to the name of the room you want to send notifications to.
19 19
 
20
-      You can provide a `username` and a `message`. When sending a HTML formatted message change `format` to "html".
21
-      If you want your message to notify the room members change `notify` to "true".
20
+      You can provide a `username` and a `message`. If you want to use mentions change `format` to "text" ([details](https://www.hipchat.com/docs/api/method/rooms/message)).
21
+
22
+      If you want your message to notify the room members change `notify` to "Yes".
23
+
22 24
       Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random")
23 25
 
24 26
       Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
@@ -41,6 +43,7 @@ module Agents
41 43
     form_configurable :message, type: :text
42 44
     form_configurable :notify, type: :boolean
43 45
     form_configurable :color, type: :array, values: ['yellow', 'red', 'green', 'purple', 'gray', 'random']
46
+    form_configurable :format, type: :array, values: ['html', 'text']
44 47
 
45 48
     def validate_auth_token
46 49
       client.rooms
@@ -65,13 +68,17 @@ module Agents
65 68
     def receive(incoming_events)
66 69
       incoming_events.each do |event|
67 70
         mo = interpolated(event)
68
-        client[mo[:room_name]].send(mo[:username][0..14], mo[:message], :notify => boolify(mo[:notify]), :color => mo[:color])
71
+        client[mo[:room_name]].send(mo[:username][0..14], mo[:message],
72
+                                      notify: boolify(mo[:notify]),
73
+                                      color: mo[:color],
74
+                                      message_format: mo[:format].presence || 'html'
75
+                                    )
69 76
       end
70 77
     end
71 78
 
72 79
     private
73 80
     def client
74
-      @client ||= HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token'))
81
+      @client ||= HipChat::Client.new(interpolated[:auth_token].presence || credential('hipchat_auth_token'))
75 82
     end
76 83
   end
77 84
 end

+ 1 - 1
app/presenters/form_configurable_agent_presenter.rb

@@ -33,7 +33,7 @@ class FormConfigurableAgentPresenter < Decorator
33 33
         end)
34 34
       end
35 35
     when :array
36
-      @view.select_tag "agent[options][#{attribute}]", @view.options_for_select(data[:values], value), html_options.merge(class: "form-control")
36
+      @view.select_tag("agent[options][#{attribute}]", @view.options_for_select(data[:values], value), html_options.merge(class: "form-control"))
37 37
     when :string
38 38
       @view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => 'form-control')
39 39
     end

+ 56 - 0
spec/concerns/form_configurable_spec.rb

@@ -0,0 +1,56 @@
1
+require 'spec_helper'
2
+
3
+describe FormConfigurable do
4
+  class Agent1
5
+    include FormConfigurable
6
+
7
+    def validate_test
8
+      true
9
+    end
10
+
11
+    def complete_test
12
+      [{name: 'test', value: 1234}]
13
+    end
14
+  end
15
+
16
+  class Agent2 < Agent
17
+  end
18
+
19
+  before(:all) do
20
+    @agent1 = Agent1.new
21
+    @agent2 = Agent2.new
22
+  end
23
+
24
+  it "#is_form_configurable" do
25
+    expect(@agent1.is_form_configurable?).to be true
26
+    expect(@agent2.is_form_configurable?).to be false
27
+  end
28
+
29
+  describe "#validete_option" do
30
+    it "should call the validation method if it is defined" do
31
+      expect(@agent1.validate_option('test')).to be true
32
+    end
33
+
34
+    it "should return false of the method is undefined" do
35
+      expect(@agent1.validate_option('undefined')).to be false
36
+    end
37
+  end
38
+
39
+  it "#complete_option" do
40
+    expect(@agent1.complete_option('test')).to eq [{name: 'test', value: 1234}]
41
+  end
42
+
43
+  describe "#form_configurable" do
44
+    it "should raise an ArgumentError for invalid  options" do
45
+      expect { Agent1.form_configurable(:test, invalid: true) }.to raise_error(ArgumentError)
46
+    end
47
+
48
+    it "should raise an ArgumentError when not providing an array with type: array" do
49
+      expect { Agent1.form_configurable(:test, type: :array, values: 1) }.to raise_error(ArgumentError)
50
+    end
51
+
52
+    it "should not require any options for the default values" do
53
+      expect { Agent1.form_configurable(:test) }.to change(Agent1, :form_configurable_attributes).by(['test'])
54
+    end
55
+  end
56
+end

+ 40 - 0
spec/controllers/agents_controller_spec.rb

@@ -307,4 +307,44 @@ describe AgentsController do
307 307
       expect(response).to redirect_to scenario_path(scenarios(:bob_weather))
308 308
     end
309 309
   end
310
+
311
+  describe "#form_configurable actions" do
312
+    before(:each) do
313
+      @params = {attribute: 'auth_token', agent: valid_attributes(:type => "Agents::HipchatAgent", options: {auth_token: '12345'})}
314
+      sign_in users(:bob)
315
+    end
316
+    describe "POST validate" do
317
+
318
+      it "returns with status 200 when called with a valid option" do
319
+        any_instance_of(Agents::HipchatAgent) do |klass|
320
+          stub(klass).validate_option { true }
321
+        end
322
+
323
+        post :validate, @params
324
+        expect(response.status).to eq 200
325
+      end
326
+
327
+      it "returns with status 403 when called with an invalid option" do
328
+        any_instance_of(Agents::HipchatAgent) do |klass|
329
+          stub(klass).validate_option { false }
330
+        end
331
+
332
+        post :validate, @params
333
+        expect(response.status).to eq 403
334
+      end
335
+    end
336
+
337
+    describe "POST complete" do
338
+      it "callsAgent#complete_option and renders json" do
339
+        any_instance_of(Agents::HipchatAgent) do |klass|
340
+          stub(klass).complete_option { [{name: 'test', value: 1}] }
341
+        end
342
+
343
+        post :complete, @params
344
+        expect(response.status).to eq 200
345
+        expect(response.header['Content-Type']).to include('application/json')
346
+
347
+      end
348
+    end
349
+  end
310 350
 end

+ 6 - 0
spec/fixtures/agents.yml

@@ -111,3 +111,9 @@ jane_basecamp_agent:
111 111
   user: jane
112 112
   service: generic
113 113
   guid: <%= SecureRandom.hex %>
114
+
115
+bob_hipchat_agent:
116
+  type: Agents::HipchatAgent
117
+  user: bob
118
+  service: generic
119
+  guid: <%= SecureRandom.hex %>

+ 28 - 5
spec/models/agents/basecamp_agent_spec.rb

@@ -5,8 +5,21 @@ describe Agents::BasecampAgent do
5 5
   it_behaves_like Oauthable
6 6
 
7 7
   before(:each) do
8
-    stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
9
-    stub_request(:get, /02:00$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
8
+    stub_request(:get, /events.json$/).to_return(
9
+      :body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")),
10
+      :status => 200,
11
+      :headers => {"Content-Type" => "text/json"}
12
+    )
13
+    stub_request(:get, /projects.json$/).to_return(
14
+      :body => JSON.dump([{name: 'test', id: 1234},{name: 'test1', id: 1235}]),
15
+      :status => 200,
16
+      :headers => {"Content-Type" => "text/json"}
17
+    )
18
+    stub_request(:get, /02:00$/).to_return(
19
+      :body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")),
20
+      :status => 200,
21
+      :headers => {"Content-Type" => "text/json"}
22
+    )
10 23
     @valid_params = { :project_id => 6789 }
11 24
 
12 25
     @checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params)
@@ -32,10 +45,13 @@ describe Agents::BasecampAgent do
32 45
       expect(@checker.send(:request_options)).to eq({:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => 'Bearer "1234token"'}})
33 46
     end
34 47
 
35
-    it "should generate the currect request url" do
36
-      expect(@checker.send(:request_url)).to eq("https://basecamp.com/12345/api/v1/projects/6789/events.json")
48
+    it "should generate the correct events url" do
49
+      expect(@checker.send(:events_url)).to eq("https://basecamp.com/12345/api/v1/projects/6789/events.json")
37 50
     end
38 51
 
52
+    it "should generate the correct projects url" do
53
+      expect(@checker.send(:projects_url)).to eq("https://basecamp.com/12345/api/v1/projects.json")
54
+    end
39 55
 
40 56
     it "should not provide the since attribute on first run" do
41 57
       expect(@checker.send(:query_parameters)).to eq({})
@@ -48,6 +64,13 @@ describe Agents::BasecampAgent do
48 64
       expect(@checker.reload.send(:query_parameters)).to eq({:query => {:since => time}})
49 65
     end
50 66
   end
67
+
68
+  describe "#complete_project_id" do
69
+    it "should return a array of hashes" do
70
+      expect(@checker.complete_project_id).to eq [{name: 'test (1234)', value: 1234}, {name: 'test1 (1235)', value: 1235}]
71
+    end
72
+  end
73
+
51 74
   describe "#check" do
52 75
     it "should not emit events on its first run" do
53 76
       expect { @checker.check }.to change { Event.count }.by(0)
@@ -60,7 +83,7 @@ describe Agents::BasecampAgent do
60 83
   end
61 84
 
62 85
   describe "#working?" do
63
-    it "it is working when at least one event was emited" do
86
+    it "it is working when at least one event was emitted" do
64 87
       expect(@checker).not_to be_working
65 88
       @checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00'
66 89
       @checker.check

+ 25 - 0
spec/models/agents/hipchat_agent_spec.rb

@@ -50,6 +50,31 @@ describe Agents::HipchatAgent do
50 50
     end
51 51
   end
52 52
 
53
+  describe "#validate_auth_token" do
54
+    it "should return true when valid" do
55
+      any_instance_of(HipChat::Client) do |klass|
56
+        stub(klass).rooms { true }
57
+      end
58
+      expect(@checker.validate_auth_token).to be true
59
+    end
60
+
61
+    it "should return false when invalid" do
62
+      any_instance_of(HipChat::Client) do |klass|
63
+        stub(klass).rooms { raise HipChat::UnknownResponseCode.new }
64
+      end
65
+      expect(@checker.validate_auth_token).to be false
66
+    end
67
+  end
68
+
69
+  describe "#complete_room_name" do
70
+    it "should return a array of hashes" do
71
+      any_instance_of(HipChat::Client) do |klass|
72
+        stub(klass).rooms { [OpenStruct.new(name: 'test'), OpenStruct.new(name: 'test1')] }
73
+      end
74
+      expect(@checker.complete_room_name).to eq [{name: 'test', value: 'test'},{name: 'test1', value: 'test1'}]
75
+    end
76
+  end
77
+
53 78
   describe "#receive" do
54 79
     it "send a message to the hipchat" do
55 80
       any_instance_of(HipChat::Room) do |obj|